<?php

namespace SixShop\News\Service;

use Exception;
use FFI;

/**
 * FFI Service Wrapper for the News extension's Go library.
 * Implements a singleton pattern to ensure the library is loaded only once.
 */
class NewsFfiService
{
    private static ?self $instance = null;
    private ?FFI $ffi = null;

    /**
     * Private constructor to prevent direct instantiation.
     * Loads the shared library and defines the C function signatures.
     */
    private function __construct()
    {

        $cdef = "
                // Initialization function
                char* Initialize(char* host, char* user, char* password, char* dbname, char* charset, int port);

                // Category functions
                char* GetCategoryList(char* whereJson);
                char* GetCategoryByID(int id);
                char* CreateCategory(char* dataJson);
                char* UpdateCategory(int id, char* dataJson);
                char* DeleteCategory(int id);

                // News functions
                char* GetNewsList(char* paramsJson);
                char* GetNewsByID(int id);
                char* CreateNews(char* dataJson);
                char* UpdateNews(int id, char* dataJson);
                char* DeleteNews(int id);
                char* IncrementNewsViews(int id);
            ";

        $libPath = __DIR__ . '/../../ffi/lib_news.so';
        if (!file_exists($libPath)) {
            $libPath = root_path('runtime') . 'lib_news.so';
            if (!file_exists($libPath)) {
                throw new Exception("FFI library not found at: $libPath. Please run 'make' in the ffi directory.");
            }
        }

        $this->ffi = FFI::cdef($cdef, $libPath);

        // Dynamically initialize the Go module with config from the environment
        $dbConfig = config('database.connections.mysql');

        $resultJson = FFI::string($this->ffi->Initialize(
            $dbConfig['hostname'],
            $dbConfig['username'],
            $dbConfig['password'],
            $dbConfig['database'],
            $dbConfig['charset'],
            $dbConfig['hostport']
        ));

        $result = json_decode($resultJson, true);
        if (!isset($result['status']) || $result['status'] !== 'ok') {
            $error = $result['error'] ?? 'unknown initialization error';
            throw new Exception("Failed to initialize Go FFI database: " . $error);
        }
    }

    /**
     * Gets the single instance of the FFI service.
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Generic private method to handle FFI calls, reducing code duplication.
     * It marshals arguments to C-compatible types and unmarshals the JSON result.
     */
    private function call(string $method, ...$args)
    {
        if ($this->ffi === null) {
            throw new Exception("FFI service is not available.");
        }

        // The Go functions return a char* which needs to be converted to a PHP string.
        $resultPtr = $this->ffi->$method(...$args);
        $resultJson = FFI::string($resultPtr);

        // In Go, we allocated the string using C.CString, which uses malloc.
        // However, the responsibility for freeing memory with FFI can be tricky.
        // Go's FFI implementation for returning `*C.char` from a Go string often
        // places it in memory that Go's GC manages. Explicitly freeing it can
        // cause a double-free panic. For this implementation, we assume Go's GC handles it.
        // C.free(FFI::addr($resultPtr)); // Do NOT do this unless you are sure.

        return json_decode($resultJson, true);
    }

    // --- Category Service Wrappers ---

    public function getCategoryList(array $where = []): array
    {
        $payload = empty($where) ? new \stdClass() : $where;
        return $this->call('GetCategoryList', json_encode($payload));
    }

    public function getCategoryById(int $id): array
    {
        return $this->call('GetCategoryByID', $id);
    }

    public function createCategory(array $data): array
    {
        $payload = empty($data) ? new \stdClass() : $data;
        return $this->call('CreateCategory', json_encode($payload));
    }

    public function updateCategory(int $id, array $data): array
    {
        $payload = empty($data) ? new \stdClass() : $data;
        return $this->call('UpdateCategory', $id, json_encode($payload));
    }

    public function deleteCategory(int $id): array
    {
        return $this->call('DeleteCategory', $id);
    }

    // --- News Service Wrappers ---

    public function getNewsList(array $params = []): array
    {
        $payload = empty($params) ? new \stdClass() : $params;
        return $this->call('GetNewsList', json_encode($payload));
    }

    public function getNewsById(int $id): array
    {
        return $this->call('GetNewsByID', $id);
    }

    public function createNews(array $data): array
    {
        $payload = empty($data) ? new \stdClass() : $data;
        $result = $this->call('CreateNews', json_encode($payload));
        return is_array($result) ? $result : [];
    }

    public function updateNews(int $id, array $data): array
    {
        $payload = empty($data) ? new \stdClass() : $data;
        return $this->call('UpdateNews', $id, json_encode($payload));
    }

    public function deleteNews(int $id): array
    {
        return $this->call('DeleteNews', $id);
    }

    public function incrementNewsViews(int $id): array
    {
        return $this->call('IncrementNewsViews', $id);
    }
} 